package umg.os.process;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.InputMismatchException;
import java.util.List;
import umg.os.kernel.Kernel;
import umg.os.process.Operation.OperationCommand;
import umg.os.process.Operation.OperationType;

public class ProcessControlBlock {

	public enum ProcessStatus {
		WAITING,
		RUNNING,
		TERMINATED;
	}

	public final String name;
	public final long arrivalTime;
	private long instructionPointer,waitingTime;
	private final Process relatedProcess;
	private ProcessStatus status;
	private List<ProcessFinishListener> processFinishListeners; 

	private ProcessControlBlock(String name, long arrivalTime, Process relatedProcess) {
		this.name = name;
		this.arrivalTime = arrivalTime;
		this.relatedProcess = relatedProcess;
		status = ProcessStatus.WAITING;
		waitingTime = 0;
		processFinishListeners = new ArrayList<ProcessFinishListener>();
		processFinishListeners.add(Kernel.getInstance());
	}
	/**
	 * Creates a {@link ProcessControlBlock} (and its respective {@link Process})
	 * based on the information contained in the <b>procFile</b>.
	 * <p>
	 * <i>
	 * Note: the file denoted by <b>procFile</b> does not necessarily has to 
	 * have the extension ".proc".
	 * <p>
	 * Structure of a proc file
	 * </i>
	 * <p>
	 * </code> 
	 * ProcessName<br>
	 * ArrivalTime<br>
	 * Actions(1+)
	 * </code>
	 *  
	 * @param procFile
	 * @return
	 * @throws IOException 
	 */
	public static ProcessControlBlock instantiateFromProcFile(Path procFile) throws IOException {

		List<String> lines = Files.readAllLines(procFile, StandardCharsets.UTF_8);

		if(lines.size()<5)
		{
			throw new InputMismatchException("The file does not contains all the required information");
		}
		OperationType type = null;
		OperationCommand command = null;
		long initialAddress=0,finalAddress = 0;
		Operation op;
		int integer = 0;
		String name = lines.get(0);
		String device= null,contentToWrite = null;
		Path path= null;
		long arrivalTime = Long.valueOf(lines.get(1));
		ArrayList<Operation> operationsOfNewProcess = new ArrayList<Operation>();
		if(lines.get(2).equals("START")&&lines.get(lines.size()-1).equals("END"))
		{
			for (int i=3;i<lines.size()-1;i++) 
			{
				String[] operationSpecs = lines.get(i).split(" ");
				if(operationSpecs.length<=2 || operationSpecs.length>2)
				{
					if(operationSpecs[0].equals( "CPU" )) {
						type = OperationType.CPU;
						integer = Integer.parseInt(operationSpecs[1]);
						op = new Operation(type, integer);
					}
					else if(operationSpecs[0].equals( "DEV" )) {
						type = OperationType.DEV;
						device = operationSpecs[1];
						path = Paths.get(operationSpecs[3]);
								
						if(operationSpecs[2].equals("READ")){
							command = OperationCommand.READ;
							initialAddress = Long.valueOf(operationSpecs[4]);
							finalAddress = Long.valueOf(operationSpecs[5]);
							op = new Operation(type, device, command, path, null, initialAddress, finalAddress);
 						}
						else if(operationSpecs[2].equals("READ")){
							command = OperationCommand.WRITE;
							contentToWrite = operationSpecs[4];
							op = new Operation(type, device, command, path, contentToWrite, 0, 0);
						}
						else{
							throw new InputMismatchException("Invalid Operation");
						}						
					}
					else {
						throw new InputMismatchException("Invalid Operation");
					}
					
					operationsOfNewProcess.add(op);
				}
				else {
					throw new InputMismatchException("Sintax of operation at line "+i+" does not contains the appropiate structure");
				}
			}
		}
		else {
			throw new InputMismatchException("The proc file requires START and END identifiers");
		}
		return new ProcessControlBlock(name, arrivalTime,new Process(operationsOfNewProcess));
	}

	public void allocateInCPU() {
		if (status.equals(ProcessStatus.TERMINATED)) {
			throw new ProcessCompletedException("The process: "+name+" is already compleated. It cannot run no longer...");
		}
		status = ProcessStatus.RUNNING;
	}
	
	public void removeFromCPU() {
		if (!status.equals(ProcessStatus.TERMINATED)) {
			status = ProcessStatus.WAITING;
		}
	}
	
	/**
	 * Based on the {@link ProcessControlBlock#instructionPointer} of this instance,
	 * determines which is the next {@link Operation} of the {@link #relatedProcess},
	 * of this class.
	 *  
	 * @return
	 * The next {@link Operation} of the {@link Process} referenced by {@link #relatedProcess},
	 * which is located at the position of the {@link #instructionPointer} at the moment of the 
	 * call to this method, such "position" is calculated by subtracting the duration of each
	 * {@link Operation} in the {@link Process} referenced by {@link #relatedProcess}, as they 
	 * are ordered in it, until it finds the {@link Operation} which subtracting it may cause
	 * 0 or a negative number, such {@link Operation} is returned. If instruction pointer is 
	 * bigger than the addition of all of the {@link Operation#duration} for each {@link Operation}
	 * in the {@link Process} {@link #relatedProcess}, this method is going to return <code>null</code>.
	 */
	public Operation getNextOperation() {
		long ipCopy = instructionPointer;
		for (Operation o : relatedProcess) {
			ipCopy -= o.duration;
			if(ipCopy <= 0) {
				return o;
			}
		}
		return null;
	}

	public void increaseInstructionPointer() {
		if (status.equals(ProcessStatus.TERMINATED)) {
			throw new ProcessCompletedException("The process: "+name+" is already compleated. It cannot run no longer...");
		}
		//System.out.print("+");
		instructionPointer++;
		if (getNextOperation() == null) {
			status = ProcessStatus.TERMINATED;
			for (ProcessFinishListener pfl : processFinishListeners) {
				pfl.processFinished();
			}
		}
	}

	public void increaseWaitingTime() {
		waitingTime++;
	}
	
	public Process getRelatedProcess() {
		return relatedProcess;
	}
	
	public ProcessStatus getStatus() {
		return status;
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("Process: "+name+"\n\tArrival Time: "+arrivalTime+
				"\n\tStatus: "+status+"\n\tOperation(s) of process: "); 
		for (Operation o : relatedProcess) {
			sb.append("\n\t\t"+o);
		}
		return sb.toString();
	}
	
	@SuppressWarnings("serial")
	private class ProcessCompletedException extends RuntimeException{
	
		private ProcessCompletedException(String message) {
			super(message);
		}
		
	}
	
	public interface ProcessFinishListener {
		
		public void processFinished(); 
		
	}
	
}
